/*
 * Licensed to the Apache Software Foundation (ASF) under one or more
 * contributor license agreements.  See the NOTICE file distributed with
 * this work for additional information regarding copyright ownership.
 * The ASF licenses this file to You under the Apache License, Version 2.0
 * (the "License"); you may not use this file except in compliance with
 * the License.  You may obtain a copy of the License at
 *
 *     http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */
package org.apache.tika.config.loader;

import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.net.URL;
import java.nio.charset.StandardCharsets;
import java.util.Collections;
import java.util.Enumeration;
import java.util.HashMap;
import java.util.LinkedHashMap;
import java.util.Map;

import org.apache.tika.exception.TikaConfigException;

/**
 * Registry for looking up Tika component classes by name.
 * Loads component name-to-class mappings from META-INF/tika/*.idx files
 * generated by the {@code @TikaComponent} annotation processor.
 * <p>
 * Also includes built-in aliases for external dependencies that cannot be
 * annotated with @TikaComponent.
 */
public class ComponentRegistry {

    /**
     * Built-in aliases for external dependencies.
     * Maps component names to fully qualified class names.
     */
    private static final Map<String, String> BUILTIN_ALIASES = createBuiltinAliases();

    private static Map<String, String> createBuiltinAliases() {
        Map<String, String> aliases = new HashMap<>();
        // Alias for org.gagravarr:vorbis-java-tika dependency
        // TODO -- make this configurable
        aliases.put("ogg-detector", "org.gagravarr.tika.OggDetector");
        return Collections.unmodifiableMap(aliases);
    }

    private final Map<String, Class<?>> components;
    private final ClassLoader classLoader;

    /**
     * Creates a component registry by loading the specified index file.
     *
     * @param indexFileName the index file name (e.g., "parsers", "detectors")
     *                      without the .idx extension
     * @param classLoader the class loader to use for loading classes
     * @throws TikaConfigException if the index file cannot be loaded
     */
    public ComponentRegistry(String indexFileName, ClassLoader classLoader)
            throws TikaConfigException {
        this.classLoader = classLoader;
        this.components = loadComponents(indexFileName);
    }

    /**
     * Looks up a component class by name.
     *
     * @param name the component name (e.g., "pdf-parser")
     * @return the component class
     * @throws TikaConfigException if the component name is not found
     */
    public Class<?> getComponentClass(String name) throws TikaConfigException {
        Class<?> clazz = components.get(name);
        if (clazz == null) {
            throw new TikaConfigException("Unknown component name: '" + name + "'. " +
                    "Available components: " + components.keySet());
        }
        return clazz;
    }

    /**
     * Returns all registered component names.
     *
     * @return unmodifiable map of component names to classes
     */
    public Map<String, Class<?>> getAllComponents() {
        return Collections.unmodifiableMap(components);
    }

    /**
     * Checks if a component with the given name is registered.
     *
     * @param name the component name
     * @return true if the component is registered
     */
    public boolean hasComponent(String name) {
        return components.containsKey(name);
    }

    private Map<String, Class<?>> loadComponents(String indexFileName)
            throws TikaConfigException {
        Map<String, Class<?>> result = new LinkedHashMap<>();
        String resourcePath = "META-INF/tika/" + indexFileName + ".idx";

        try {
            Enumeration<URL> resources = classLoader.getResources(resourcePath);

            if (!resources.hasMoreElements()) {
                throw new TikaConfigException("Component index file not found: " + resourcePath);
            }

            while (resources.hasMoreElements()) {
                URL url = resources.nextElement();
                loadFromUrl(url, result);
            }

        } catch (IOException e) {
            throw new TikaConfigException("Failed to load component index: " + resourcePath, e);
        }

        // Load built-in aliases for external dependencies
        loadBuiltinAliases(result);

        return result;
    }

    private void loadBuiltinAliases(Map<String, Class<?>> result) {
        for (Map.Entry<String, String> alias : BUILTIN_ALIASES.entrySet()) {
            try {
                Class<?> clazz = Class.forName(alias.getValue(), false, classLoader);
                result.put(alias.getKey(), clazz);
            } catch (ClassNotFoundException e) {
                // External dependency not on classpath - skip this alias
                // This is expected behavior, not an error
            }
        }
    }

    private void loadFromUrl(URL url, Map<String, Class<?>> result) throws TikaConfigException {
        try (InputStream in = url.openStream();
                BufferedReader reader = new BufferedReader(
                        new InputStreamReader(in, StandardCharsets.UTF_8))) {

            String line;
            int lineNumber = 0;

            while ((line = reader.readLine()) != null) {
                lineNumber++;
                line = line.trim();

                // Skip comments and empty lines
                if (line.isEmpty() || line.startsWith("#")) {
                    continue;
                }

                // Parse: component-name=fully.qualified.ClassName
                int equalsIndex = line.indexOf('=');
                if (equalsIndex == -1) {
                    throw new TikaConfigException(
                            "Invalid index file format at " + url + " line " + lineNumber +
                            ": expected 'name=class', got: " + line);
                }

                String name = line.substring(0, equalsIndex).trim();
                String className = line.substring(equalsIndex + 1).trim();

                if (name.isEmpty() || className.isEmpty()) {
                    throw new TikaConfigException(
                            "Invalid index file format at " + url + " line " + lineNumber +
                            ": name or class is empty");
                }

                // Load the class
                try {
                    Class<?> clazz = classLoader.loadClass(className);
                    result.put(name, clazz);
                } catch (ClassNotFoundException e) {
                    throw new TikaConfigException(
                            "Component class not found: " + className + " (from " + url + ")", e);
                }
            }

        } catch (IOException e) {
            throw new TikaConfigException("Failed to read component index from: " + url, e);
        }
    }
}
